Skip to content

PYTHON-5744 - [PoC] Client Side Support for OpenTelemetry#2720

Draft
NoahStapp wants to merge 6 commits intomongodb:masterfrom
NoahStapp:PYTHON-5052-poc
Draft

PYTHON-5744 - [PoC] Client Side Support for OpenTelemetry#2720
NoahStapp wants to merge 6 commits intomongodb:masterfrom
NoahStapp:PYTHON-5052-poc

Conversation

@NoahStapp
Copy link
Contributor

@NoahStapp NoahStapp commented Mar 4, 2026

PYTHON-5744

Changes in this PR

A PoC for implementing OpenTelemetry support in PyMongo.

EDIT: I'm going to split the logging + monitoring refactor into a separate ticket. That maintains better separation of concerns and clarity. This PR will remain open as the artifact produced by the PoC work.

The bulk of the changes here are a refactor to consolidate our existing command logging and monitoring code as part of the addition of OpenTelemetry. The old logging and monitoring calls had a lot of duplication and reduced clarity for the code surrounding them. With this refactor, all handling of telemetry is centralized and streamlined, improving readability and maintainability.

Test Plan

All current tests pass. The OpenTelemetry unified tests detailed in DRIVERS-719 are not implemented here in the PoC.

An example OpenTelemetry span for an insert operation:

{
    "name": "insert",
    "context": {
        "trace_id": "0xe25a92db9a4128e88cb51d6a9b400733",
        "span_id": "0x86d36f6e2b948ea0",
        "trace_state": "[]"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": null,
    "start_time": "2026-03-04T21:07:37.254308Z",
    "end_time": "2026-03-04T21:07:37.286829Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "db.system": "mongodb",
        "db.namespace": "otel_test_db",
        "db.command.name": "insert",
        "db.query.summary": "insert otel_test_db.test_collection",
        "server.address": "localhost",
        "server.port": 27017,
        "network.transport": "tcp",
        "db.mongodb.driver_connection_id": 1,
        "db.collection.name": "test_collection",
        "db.mongodb.server_connection_id": 10
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.39.1",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is there test coverage?
  • Is any followup work tracked in a JIRA ticket? If so, add link(s).

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

@NoahStapp NoahStapp changed the title PYTHON-5052 - [PoC] Client Side Support for OpenTelemetry PYTHON-5744 - [PoC] Client Side Support for OpenTelemetry Mar 4, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a PoC for OpenTelemetry support in PyMongo by centralizing all command telemetry (logging, APM event publishing, and OTel spans) into a new pymongo/telemetry.py module. The refactor eliminates significant code duplication that existed across synchronous and asynchronous variants of server.py, network.py, bulk.py, and client_bulk.py.

Changes:

  • Adds pymongo/telemetry.py with _CommandTelemetry class and command_telemetry() factory that unifies logging, APM event publishing, and OpenTelemetry span lifecycle
  • Refactors all command telemetry call sites (8 files: sync/async variants of server.py, network.py, bulk.py, client_bulk.py) to use the new centralized telemetry
  • Adds opentelemetry-api>=1.20.0 as an optional dependency via a new opentelemetry extras group

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pymongo/telemetry.py New module centralizing command telemetry (logging, APM events, OTel spans)
pymongo/synchronous/network.py Replaces inline telemetry with command_telemetry() context manager
pymongo/asynchronous/network.py Same as synchronous counterpart
pymongo/synchronous/server.py Replaces inline telemetry with command_telemetry() context manager
pymongo/asynchronous/server.py Same as synchronous counterpart
pymongo/synchronous/bulk.py Replaces inline telemetry with command_telemetry() context manager
pymongo/asynchronous/bulk.py Same as synchronous counterpart
pymongo/synchronous/client_bulk.py Replaces inline telemetry with command_telemetry() context manager
pymongo/asynchronous/client_bulk.py Same as synchronous counterpart
pyproject.toml Adds opentelemetry extras group
requirements/opentelemetry.txt New file with opentelemetry-api>=1.20.0
uv.lock Lock file updated with opentelemetry-api 1.39.1
Comments suppressed due to low confidence (1)

pymongo/telemetry.py:416

  • No tests exist for the new pymongo/telemetry.py module. The project has extensive test coverage for monitoring behavior (e.g., test/test_monitoring.py). Given that this PR refactors all command monitoring/logging into a centralized _CommandTelemetry class, the new module should have unit tests covering:
  • The _extract_collection_name function for various command types
  • _is_tracing_enabled and _get_tracer behavior
  • _CommandTelemetry lifecycle (started, succeeded, failed events)
  • OTel span attribute setting and error recording
# Copyright 2026-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unified telemetry support for PyMongo.

Supports telemetry through standardized logging, event publishing, and OpenTelemetry.

To enable OpenTelemetry logging, set the environment variable:
    OTEL_PYTHON_INSTRUMENTATION_MONGODB_ENABLED=true

.. versionadded:: 4.x
"""
from __future__ import annotations

import logging
import os
from datetime import datetime
from typing import TYPE_CHECKING, Any, Mapping, Optional

from pymongo import message
from pymongo.errors import NotPrimaryError, OperationFailure
from pymongo.logger import _COMMAND_LOGGER, _SENSITIVE_COMMANDS, _CommandStatusMessage, _debug_log
from pymongo.monitoring import _EventListeners

try:
    from opentelemetry import trace  # type: ignore[import-not-found]
    from opentelemetry.trace import (  # type: ignore[import-not-found]
        Span,
        SpanKind,
        Status,
        StatusCode,
        Tracer,
    )

    _HAS_OPENTELEMETRY = True
except ImportError:
    _HAS_OPENTELEMETRY = False
    trace = None
    Span = None
    SpanKind = None
    Status = None
    StatusCode = None
    Tracer = None

if TYPE_CHECKING:
    from pymongo.typings import _Address, _AgnosticMongoClient, _DocumentOut


_OTEL_ENABLED_ENV = "OTEL_PYTHON_INSTRUMENTATION_MONGODB_ENABLED"


def _is_tracing_enabled() -> bool:
    if not _HAS_OPENTELEMETRY:
        return False
    value = os.environ.get(_OTEL_ENABLED_ENV, "").lower()
    return value in ("1", "true")


def _get_tracer() -> Optional[Tracer]:
    if not _HAS_OPENTELEMETRY or not _is_tracing_enabled():
        return None
    from pymongo._version import __version__

    return trace.get_tracer("PyMongo", __version__)


def _is_sensitive_command(command_name: str) -> bool:
    return command_name.lower() in _SENSITIVE_COMMANDS


def _build_query_summary(
    command_name: str,
    database_name: str,
    collection_name: Optional[str],
) -> str:
    """Build the db.query.summary attribute value."""
    if collection_name:
        return f"{command_name} {database_name}.{collection_name}"
    return f"{command_name} {database_name}"


def _extract_collection_name(spec: Mapping[str, Any]) -> Optional[str]:
    """Extract collection name from command spec if applicable."""
    if not spec:
        return None
    cmd_name = next(iter(spec)).lower()
    # Commands where the first value is the collection name
    if cmd_name in (
        "insert",
        "update",
        "delete",
        "find",
        "aggregate",
        "findandmodify",
        "count",
        "distinct",
        "create",
        "drop",
        "createindexes",
        "dropindexes",
        "listindexes",
    ):
        value = spec.get(next(iter(spec)))
        if isinstance(value, str):
            return value
    return None


class _CommandTelemetry:
    """Manages telemetry for MongoDB commands, including logging, event publishing, and OpenTelemetry spans.

    This class is a context manager that handles the full lifecycle of command telemetry:
    - On entry: sets up OpenTelemetry span (if enabled) and publishes the started event and/or log
    - On exit: cleans up the span context (caller handles success/failure publishing)
    """

    __slots__ = (
        "_command_name",
        "_database_name",
        "_spec",
        "_driver_connection_id",
        "_server_connection_id",
        "_publish_event",
        "_start_time",
        "_address",
        "_listeners",
        "_client",
        "_request_id",
        "_operation_id",
        "_service_id",
        "_span",
        "_span_context",
    )

    def __init__(
        self,
        command_name: str,
        database_name: str,
        spec: Mapping[str, Any],
        driver_connection_id: int,
        server_connection_id: Optional[int],
        publish_event: bool,
        start_time: datetime,
        address: _Address,
        listeners: Optional[_EventListeners],
        client: Optional[_AgnosticMongoClient],
        request_id: int,
        service_id: Optional[Any],
        operation_id: Optional[int] = None,
    ):
        self._command_name = command_name
        self._database_name = database_name
        self._spec = spec
        self._driver_connection_id = driver_connection_id
        self._server_connection_id = server_connection_id
        self._publish_event = publish_event
        self._start_time = start_time
        self._address = address
        self._listeners = listeners
        self._client = client
        self._request_id = request_id
        self._operation_id = operation_id if operation_id is not None else request_id
        self._service_id = service_id
        self._span: Optional[Span] = None
        self._span_context: Optional[Any] = None

    def __enter__(self) -> _CommandTelemetry:
        self._setup_span()
        self.publish_started()
        return self

    def __exit__(
        self,
        exc_type: Optional[type],
        exc_val: Optional[BaseException],
        exc_tb: Optional[Any],
    ) -> None:
        if self._span_context is not None:
            self._span_context.__exit__(exc_type, exc_val, exc_tb)

    def _setup_span(self) -> None:
        """Set up OpenTelemetry span if tracing is enabled and command is not sensitive."""
        tracer = _get_tracer()

        if tracer is None or _is_sensitive_command(self._command_name):
            return

        collection_name = _extract_collection_name(self._spec)
        query_summary = _build_query_summary(
            self._command_name, self._database_name, collection_name
        )

        self._span_context = tracer.start_as_current_span(
            name=self._command_name,
            kind=SpanKind.CLIENT,
        )
        self._span = self._span_context.__enter__()

        self._span.set_attribute("db.system", "mongodb")
        self._span.set_attribute("db.namespace", self._database_name)
        self._span.set_attribute("db.command.name", self._command_name)
        self._span.set_attribute("db.query.summary", query_summary)
        if self._address:
            self._span.set_attribute("server.address", self._address[0])
            self._span.set_attribute("server.port", self._address[1])
        self._span.set_attribute("network.transport", "tcp")
        self._span.set_attribute("db.mongodb.driver_connection_id", self._driver_connection_id)

        if collection_name:
            self._span.set_attribute("db.collection.name", collection_name)
        if self._server_connection_id is not None:
            self._span.set_attribute("db.mongodb.server_connection_id", self._server_connection_id)

    @property
    def span(self) -> Optional[Span]:
        """Return the OpenTelemetry span, or None if tracing is disabled."""
        return self._span

    def publish_started(self) -> None:
        """Publish command started event and log if enabled."""
        if self._client is not None:
            if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
                _debug_log(
                    _COMMAND_LOGGER,
                    message=_CommandStatusMessage.STARTED,
                    clientId=self._client._topology_settings._topology_id,
                    command=self._spec,
                    commandName=next(iter(self._spec)),
                    databaseName=self._database_name,
                    requestId=self._request_id,
                    operationId=self._operation_id,
                    driverConnectionId=self._driver_connection_id,
                    serverConnectionId=self._server_connection_id,
                    serverHost=self._address[0] if self._address else None,
                    serverPort=self._address[1] if self._address else None,
                    serviceId=self._service_id,
                )
        if self._publish_event:
            assert self._listeners is not None
            assert self._address is not None
            self._listeners.publish_command_start(
                self._spec,  # type: ignore[arg-type]
                self._database_name,
                self._request_id,
                self._address,
                self._server_connection_id,
                op_id=self._operation_id,
                service_id=self._service_id,
            )

    def publish_succeeded(
        self,
        reply: _DocumentOut,
        speculative_hello: bool = False,
        speculative_authenticate: bool = False,
    ) -> None:
        """Publish command succeeded event and log if enabled."""
        duration = datetime.now() - self._start_time

        # Add cursor_id to span if present in response
        if self._span is not None and isinstance(reply, dict):
            cursor_info = reply.get("cursor")
            if cursor_info and isinstance(cursor_info, dict):
                cursor_id = cursor_info.get("id", 0)
                if cursor_id:
                    self._span.set_attribute("db.mongodb.cursor_id", cursor_id)

        if self._client is not None:
            if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
                _debug_log(
                    _COMMAND_LOGGER,
                    message=_CommandStatusMessage.SUCCEEDED,
                    clientId=self._client._topology_settings._topology_id,
                    durationMS=duration,
                    reply=reply,
                    commandName=next(iter(self._spec)),
                    databaseName=self._database_name,
                    requestId=self._request_id,
                    operationId=self._operation_id,
                    driverConnectionId=self._driver_connection_id,
                    serverConnectionId=self._server_connection_id,
                    serverHost=self._address[0] if self._address else None,
                    serverPort=self._address[1] if self._address else None,
                    serviceId=self._service_id,
                    speculative_authenticate=speculative_authenticate,
                )
        if self._publish_event:
            assert self._listeners is not None
            assert self._address is not None
            self._listeners.publish_command_success(
                duration,
                reply,
                self._command_name,
                self._request_id,
                self._address,
                self._server_connection_id,
                op_id=self._operation_id,
                service_id=self._service_id,
                speculative_hello=speculative_hello,
                database_name=self._database_name,
            )

    def publish_failed(self, exc: Exception) -> None:
        """Publish command failed event and log if enabled."""
        duration = datetime.now() - self._start_time
        if isinstance(exc, (NotPrimaryError, OperationFailure)):
            failure: _DocumentOut = exc.details  # type: ignore[assignment]
        else:
            failure = message._convert_exception(exc)

        if self._span is not None:
            error_code = getattr(exc, "code", None)
            self._span.record_exception(exc)
            self._span.set_status(Status(StatusCode.ERROR, str(exc)))

            if error_code is not None:
                self._span.set_attribute("db.response.status_code", str(error_code))
        if self._client is not None:
            if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
                _debug_log(
                    _COMMAND_LOGGER,
                    message=_CommandStatusMessage.FAILED,
                    clientId=self._client._topology_settings._topology_id,
                    durationMS=duration,
                    failure=failure,
                    commandName=next(iter(self._spec)),
                    databaseName=self._database_name,
                    requestId=self._request_id,
                    operationId=self._operation_id,
                    driverConnectionId=self._driver_connection_id,
                    serverConnectionId=self._server_connection_id,
                    serverHost=self._address[0] if self._address else None,
                    serverPort=self._address[1] if self._address else None,
                    serviceId=self._service_id,
                    isServerSideError=isinstance(exc, OperationFailure),
                )
        if self._publish_event:
            assert self._listeners is not None
            assert self._address is not None
            self._listeners.publish_command_failure(
                duration,
                failure,
                self._command_name,
                self._request_id,
                self._address,
                self._server_connection_id,
                op_id=self._operation_id,
                service_id=self._service_id,
                database_name=self._database_name,
            )


def command_telemetry(
    command_name: str,
    database_name: str,
    spec: Mapping[str, Any],
    driver_connection_id: int,
    server_connection_id: Optional[int],
    publish_event: bool,
    start_time: datetime,
    request_id: int,
    address: _Address,
    listeners: Optional[_EventListeners] = None,
    client: Optional[_AgnosticMongoClient] = None,
    service_id: Optional[Any] = None,
    operation_id: Optional[int] = None,
) -> _CommandTelemetry:
    """Create a _CommandTelemetry context manager for command telemetry.

    Returns a _CommandTelemetry instance that should be used as a context manager.
    The context manager automatically:
    - Sets up OpenTelemetry span if tracing is enabled and command is not sensitive
    - Publishes the started event and/or log on entry if enabled
    - Cleans up the span context on exit

    The caller is responsible for calling publish_succeeded() on successful completion
    and publish_failed() if an exception occurs.

    Example usage::

        with command_telemetry(...) as telemetry:
            try:
                # execute command
                result = execute_command()
            except Exception as exc:
                telemetry.publish_failed(exc)
                raise
            telemetry.publish_succeeded(result)
    """
    return _CommandTelemetry(
        command_name=command_name,
        database_name=database_name,
        spec=spec,
        driver_connection_id=driver_connection_id,
        server_connection_id=server_connection_id,
        publish_event=publish_event,
        start_time=start_time,
        address=address,
        listeners=listeners,
        client=client,
        request_id=request_id,
        service_id=service_id,
        operation_id=operation_id,
    )


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

To enable OpenTelemetry logging, set the environment variable:
OTEL_PYTHON_INSTRUMENTATION_MONGODB_ENABLED=true

.. versionadded:: 4.x
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .. versionadded:: 4.x docstring contains a placeholder version number 4.x rather than the actual version number. This should be replaced with the correct version string when the feature is released (e.g., 4.15 or whatever the next release version will be).

Suggested change
.. versionadded:: 4.x
.. versionadded:: 4.15

Copilot uses AI. Check for mistakes.

self._span_context = tracer.start_as_current_span(
name=self._command_name,
kind=SpanKind.CLIENT,
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an exception is raised and re-raised (as in server.py, network.py, and bulk.py), the OTel span records the exception twice:

  1. publish_failed(exc) explicitly calls self._span.record_exception(exc) and self._span.set_status(Status(StatusCode.ERROR, ...))
  2. The re-raised exception propagates out of the with block, causing __exit__ to call self._span_context.__exit__(exc_type, exc_val, exc_tb). Since start_as_current_span defaults to record_exception=True and set_status_on_exception=True, the OpenTelemetry SDK records the exception a second time.

Fix: Use record_exception=False, set_status_on_exception=False in the start_as_current_span() call since exception handling is done manually in publish_failed.

Suggested change
kind=SpanKind.CLIENT,
kind=SpanKind.CLIENT,
record_exception=False,
set_status_on_exception=False,

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +75

def _is_tracing_enabled() -> bool:
if not _HAS_OPENTELEMETRY:
return False
value = os.environ.get(_OTEL_ENABLED_ENV, "").lower()
return value in ("1", "true")


def _get_tracer() -> Optional[Tracer]:
if not _HAS_OPENTELEMETRY or not _is_tracing_enabled():
return None
from pymongo._version import __version__

return trace.get_tracer("PyMongo", __version__)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _is_tracing_enabled() function calls os.environ.get() on every single MongoDB command. Since _get_tracer() calls _is_tracing_enabled() and is called from _setup_span() for every command, this results in an environment variable lookup for each command, even when OpenTelemetry is installed but tracing is disabled. This adds unnecessary overhead in high-throughput applications. Consider caching the result of _is_tracing_enabled() or the tracer instance at module load/startup time, or at least check _HAS_OPENTELEMETRY first (which is already done) to short-circuit before the env lookup when OpenTelemetry is not installed. When it IS installed, every command still incurs the env lookup overhead.

Suggested change
def _is_tracing_enabled() -> bool:
if not _HAS_OPENTELEMETRY:
return False
value = os.environ.get(_OTEL_ENABLED_ENV, "").lower()
return value in ("1", "true")
def _get_tracer() -> Optional[Tracer]:
if not _HAS_OPENTELEMETRY or not _is_tracing_enabled():
return None
from pymongo._version import __version__
return trace.get_tracer("PyMongo", __version__)
# Cache for whether tracing is enabled, to avoid repeated environment lookups.
_OTEL_TRACING_ENABLED: Optional[bool] = None
# Cache for the OpenTelemetry tracer instance.
_TRACER: Optional[Tracer] = None
def _is_tracing_enabled() -> bool:
"""Return whether OpenTelemetry tracing is enabled for PyMongo.
The result is cached to avoid an environment variable lookup on every
command, which can be expensive in high-throughput applications.
"""
global _OTEL_TRACING_ENABLED
if _OTEL_TRACING_ENABLED is not None:
return _OTEL_TRACING_ENABLED
if not _HAS_OPENTELEMETRY:
_OTEL_TRACING_ENABLED = False
return _OTEL_TRACING_ENABLED
value = os.environ.get(_OTEL_ENABLED_ENV, "").lower()
_OTEL_TRACING_ENABLED = value in ("1", "true")
return _OTEL_TRACING_ENABLED
def _get_tracer() -> Optional[Tracer]:
"""Return a cached OpenTelemetry tracer for PyMongo, if tracing is enabled."""
global _TRACER
if not _HAS_OPENTELEMETRY or not _is_tracing_enabled():
return None
if _TRACER is None:
from pymongo._version import __version__
_TRACER = trace.get_tracer("PyMongo", __version__)
return _TRACER

Copilot uses AI. Check for mistakes.
with command_telemetry(
command_name=name,
database_name=dbname,
spec=spec,
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original code, publish_command_start was called with orig (the original command spec before mongos $readPreference modifications), not the potentially modified spec. The new command_telemetry call passes spec=spec, which may have been wrapped with $query/$readPreference (when is_mongos and not use_op_msg). This is a behavioral change for APM command monitoring events on mongos connections using the legacy OP_QUERY protocol: the started event will include the $readPreference-wrapped spec instead of the original command document. The fix is to pass spec=orig instead of spec=spec to command_telemetry. The same issue exists in pymongo/asynchronous/network.py.

Suggested change
spec=spec,
spec=orig,

Copilot uses AI. Check for mistakes.
with command_telemetry(
command_name=name,
database_name=dbname,
spec=spec,
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the issue in pymongo/synchronous/network.py: spec=spec is passed to command_telemetry, but the original code used orig for publish_command_start. When is_mongos and not use_op_msg, spec may be a $query-wrapped version of the original command. The spec=orig should be passed instead to preserve the original APM command monitoring behavior.

Suggested change
spec=spec,
spec=orig,

Copilot uses AI. Check for mistakes.
@codecov-commenter
Copy link

codecov-commenter commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 71.68142% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.51%. Comparing base (469a32a) to head (d1f3f4d).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
pymongo/telemetry.py 58.77% 49 Missing and 5 partials ⚠️
pymongo/asynchronous/server.py 67.74% 8 Missing and 2 partials ⚠️
pymongo/synchronous/server.py 67.74% 8 Missing and 2 partials ⚠️
pymongo/asynchronous/bulk.py 77.27% 4 Missing and 1 partial ⚠️
pymongo/asynchronous/client_bulk.py 79.16% 4 Missing and 1 partial ⚠️
pymongo/synchronous/bulk.py 77.27% 4 Missing and 1 partial ⚠️
pymongo/synchronous/client_bulk.py 79.16% 4 Missing and 1 partial ⚠️
pymongo/asynchronous/network.py 96.29% 0 Missing and 1 partial ⚠️
pymongo/synchronous/network.py 96.29% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2720      +/-   ##
==========================================
- Coverage   87.52%   87.51%   -0.02%     
==========================================
  Files         141      142       +1     
  Lines       24098    24021      -77     
  Branches     4118     4050      -68     
==========================================
- Hits        21091    21021      -70     
- Misses       2117     2119       +2     
+ Partials      890      881       -9     
Flag Coverage Δ
auth-aws-rhel8-test-auth-aws-rapid-web-identity-python3.14-cov 35.38% <28.90%> (+0.29%) ⬆️
auth-aws-win64-test-auth-aws-rapid-web-identity-python3.14-cov 35.40% <28.90%> (+0.32%) ⬆️
auth-enterprise-macos-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.98% <42.18%> (+0.25%) ⬆️
auth-enterprise-rhel8-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.98% <42.18%> (+0.25%) ⬆️
auth-enterprise-win64-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.99% <42.18%> (+0.25%) ⬆️
auth-oidc-local-ubuntu-22-test-auth-oidc-default 48.84% <48.67%> (+0.12%) ⬆️
compression-snappy-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.30% <47.19%> (+0.12%) ⬆️
compression-snappy-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
compression-snappy-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.13%) ⬆️
compression-snappy-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.09%) ⬆️
compression-zlib-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.29% <47.19%> (+0.11%) ⬆️
compression-zlib-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
compression-zlib-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.13%) ⬆️
compression-zlib-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.11%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.29% <47.19%> (+0.11%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.13%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.11%) ⬆️
compression-zstd-ubuntu-22-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.91% <47.49%> (+0.12%) ⬆️
coverage-report-coverage-report 87.50% <71.68%> (+<0.01%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.29% <47.19%> (+0.12%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.14%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.11%) ⬆️
encryption-crypt_shared-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.96% <49.26%> (+0.12%) ⬆️
encryption-crypt_shared-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.91% <50.44%> (+0.18%) ⬆️
encryption-crypt_shared-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.70% <50.44%> (+0.15%) ⬆️
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.97% <49.26%> (+0.13%) ⬆️
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.79% <50.44%> (+0.16%) ⬆️
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.69% <50.44%> (+0.15%) ⬆️
encryption-crypt_shared-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.85% <49.26%> (+0.08%) ⬆️
encryption-crypt_shared-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.90% <50.44%> (+0.16%) ⬆️
encryption-crypt_shared-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.77% <50.44%> (+0.14%) ⬆️
encryption-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.96% <49.26%> (+0.14%) ⬆️
encryption-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.91% <50.44%> (+0.14%) ⬆️
encryption-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.69% <50.44%> (+0.14%) ⬆️
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.63% <49.26%> (+0.12%) ⬆️
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 55.47% <50.44%> (+0.16%) ⬆️
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 55.37% <50.44%> (+0.10%) ⬆️
encryption-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.96% <49.26%> (+0.13%) ⬆️
encryption-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.78% <50.44%> (+0.16%) ⬆️
encryption-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.69% <50.44%> (+0.15%) ⬆️
encryption-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.90% <49.26%> (+0.20%) ⬆️
encryption-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.83% <50.44%> (+0.08%) ⬆️
encryption-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.74% <50.44%> (+0.12%) ⬆️
load-balancer-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 48.49% <46.31%> (+0.17%) ⬆️
mongodb-latest-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 57.05% <46.31%> (+0.13%) ⬆️
mongodb-latest-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 55.32% <47.19%> (+0.11%) ⬆️
mongodb-latest-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.24% <46.31%> (+0.14%) ⬆️
mongodb-latest-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.39% <47.49%> (+0.14%) ⬆️
mongodb-latest-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 57.18% <47.49%> (+0.13%) ⬆️
mongodb-rapid-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 57.05% <46.31%> (+0.12%) ⬆️
mongodb-rapid-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 55.31% <47.19%> (+0.10%) ⬆️
mongodb-rapid-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.24% <46.31%> (+0.14%) ⬆️
mongodb-rapid-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.39% <47.49%> (+0.14%) ⬆️
mongodb-rapid-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 57.19% <47.49%> (+0.14%) ⬆️
mongodb-v4.2-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 54.80% <41.00%> (+0.19%) ⬆️
mongodb-v4.2-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 53.35% <41.88%> (+0.21%) ⬆️
mongodb-v4.2-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 56.99% <41.00%> (+0.19%) ⬆️
mongodb-v4.2-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.11% <42.18%> (+0.20%) ⬆️
mongodb-v4.2-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 54.95% <42.18%> (+0.19%) ⬆️
mongodb-v4.4-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 55.21% <41.00%> (+0.20%) ⬆️
mongodb-v4.4-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 53.61% <41.88%> (+0.20%) ⬆️
mongodb-v4.4-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.39% <41.00%> (+0.19%) ⬆️
mongodb-v4.4-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.48% <42.18%> (+0.20%) ⬆️
mongodb-v4.4-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 55.25% <42.18%> (+0.18%) ⬆️
mongodb-v5.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 55.37% <41.00%> (+0.17%) ⬆️
mongodb-v5.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 53.78% <41.88%> (+0.19%) ⬆️
mongodb-v5.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.59% <41.00%> (+0.19%) ⬆️
mongodb-v5.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.72% <42.18%> (+0.20%) ⬆️
mongodb-v5.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 55.50% <42.18%> (+0.19%) ⬆️
mongodb-v6.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 55.39% <41.00%> (+0.17%) ⬆️
mongodb-v6.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 53.78% <41.88%> (+0.18%) ⬆️
mongodb-v6.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.61% <41.00%> (+0.19%) ⬆️
mongodb-v6.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.77% <42.18%> (+0.20%) ⬆️
mongodb-v6.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 55.53% <42.18%> (+0.17%) ⬆️
mongodb-v7.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 55.38% <41.00%> (+0.17%) ⬆️
mongodb-v7.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 53.76% <41.88%> (+0.17%) ⬆️
mongodb-v7.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.60% <41.00%> (+0.18%) ⬆️
mongodb-v7.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.75% <42.18%> (+0.18%) ⬆️
mongodb-v7.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 55.52% <42.18%> (+0.17%) ⬆️
mongodb-v8.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 57.05% <46.31%> (+0.12%) ⬆️
mongodb-v8.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 55.32% <47.19%> (+0.11%) ⬆️
mongodb-v8.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.24% <46.31%> (+0.14%) ⬆️
mongodb-v8.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.39% <47.49%> (+0.14%) ⬆️
mongodb-v8.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 57.18% <47.49%> (+0.15%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 56.51% <47.19%> (+0.11%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 58.47% <47.49%> (+0.14%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 57.87% <46.31%> (+0.14%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 56.15% <47.49%> (+0.11%) ⬆️
ocsp-rhel8-test-ocsp-ecdsa-valid-cert-server-staples-latest-python3.14-cov 34.34% <24.48%> (?)
ocsp-rhel8-test-ocsp-rsa-valid-cert-server-staples-latest-python3.14-cov 34.33% <24.48%> (?)
pyopenssl-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.15% <47.49%> (+0.14%) ⬆️
pyopenssl-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.15% <47.49%> (+0.14%) ⬆️
pyopenssl-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.09% <47.49%> (+0.14%) ⬆️
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.31% <47.19%> (+0.13%) ⬆️
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.11%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.28% <47.19%> (+0.13%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.53% <46.31%> (+0.14%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.91% <47.49%> (+0.11%) ⬆️
storage-inmemory-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.30% <47.19%> (+0.12%) ⬆️
storage-inmemory-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.93% <47.49%> (+0.11%) ⬆️
test-macos-arm64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.28% <46.60%> (+0.12%) ⬆️
test-macos-arm64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
test-macos-arm64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.14%) ⬆️
test-macos-arm64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.90% <46.90%> (+0.12%) ⬆️
test-macos-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.27% <46.60%> (+0.12%) ⬆️
test-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.24% <47.49%> (+0.14%) ⬆️
test-macos-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.65% <46.31%> (+0.12%) ⬆️
test-macos-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.90% <46.90%> (+0.12%) ⬆️
test-numpy-macos-arm64-test-numpy-python3.14-python3.14-cov 32.48% <11.50%> (+0.21%) ⬆️
test-numpy-macos-test-numpy-python3.14-python3.14-cov 32.47% <11.50%> (+0.20%) ⬆️
test-numpy-rhel8-test-numpy-python3.14-python3.14-cov 32.48% <11.50%> (+0.21%) ⬆️
test-numpy-win32-test-numpy-python3.14-python3.14-cov 32.45% <11.50%> (+0.20%) ⬆️
test-numpy-win64-test-numpy-python3.14-python3.14-cov 32.45% <11.50%> (+0.20%) ⬆️
test-win32-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.14% <46.60%> (+0.12%) ⬆️
test-win32-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.18% <47.49%> (+0.14%) ⬆️
test-win32-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.58% <46.31%> (+0.13%) ⬆️
test-win32-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.78% <46.90%> (+0.13%) ⬆️
test-win64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 55.14% <46.60%> (+0.11%) ⬆️
test-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 57.18% <47.49%> (+0.14%) ⬆️
test-win64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 56.58% <46.31%> (+0.13%) ⬆️
test-win64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 54.77% <46.90%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants